package com.ecnu.sei.app;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ecnu.sei.app.course.ICourse;
import com.ecnu.sei.app.resources.Classroom;
import com.ecnu.sei.app.resources.Time;
import com.ecnu.sei.app.teacher.ITeacher;
import com.ecnu.sei.app.teacher.Official;
import com.ecnu.sei.app.util.Pair;
import com.ecnu.sei.app.util.Utils.CollegeYear;
import com.ecnu.sei.app.util.Utils.DayHour;

/*
 * in memory containers to keep free slots and detect collisions
 */

public class Scheduler {

  private static Scheduler instance = null;

  private static final Logger LOG = LoggerFactory.getLogger(Scheduler.class);

  private Set<ICourse> courses = new HashSet<ICourse>();

  private List<ITeacher> teachers = new ArrayList<ITeacher>();

  private List<Official> officials = new ArrayList<Official>();

  // list of backup proctors in case that one takes a leave 
  private List<ITeacher> backups = new ArrayList<ITeacher>();

  // free resources for compulsory courses
  private Map<DayHour, Map<Time, Set<Classroom>>> free = new HashMap<DayHour, Map<Time, Set<Classroom>>>();

  // default schedule for compulsory courses
  private Map<CollegeYear, DayHour> defaultSchedule = new HashMap<CollegeYear, DayHour>();

  // exam schedule of a college year
  private Map<CollegeYear, Set<Time>> cySchedule = new HashMap<CollegeYear, Set<Time>>();

  private int patrolIndex = 0;
 
  private int backupIndex = 0;
  
  public long electiveStartTime;

  public long compulsoryStartTime;


  static {
    instance = new Scheduler(); // singleton
    LOG.info("scheduler inited");

  }

  private Scheduler() {
    defaultSchedule.put(CollegeYear.FRESHMAN, DayHour.AFTN_TWO);
    defaultSchedule.put(CollegeYear.SEPHOMORE, DayHour.AFTN_ONE);
    defaultSchedule.put(CollegeYear.JUNIOR, DayHour.MORN_TWO);
    defaultSchedule.put(CollegeYear.SENIOR, DayHour.MORN_ONE);

  }

  public static Scheduler getScheduler() {
    return instance;
  }



  public void generateSchedule() {

    // courses that could not get resources or proctors after first run
    Set<ICourse> pending = new HashSet<ICourse>();

    // we firstly set resource according to default schedule
    LOG.info("set time and classroom, for all");
    for (ICourse course : courses) {
      if (!course.setResource()) {
        pending.add(course);
      }
    }

    LOG.info("set time and classroom, for pending courses");
    for (ICourse course : pending) {
      course.setResourcePending();
    }

    pending = new HashSet<ICourse>();

    LOG.info("set proctors, firstly course teachers");
    for (ICourse course : courses) {
      ITeacher teacher = course.getCourseInfo().getTeacher(); 
      course.addProctor(teacher);
      if (!course.hasEnoughProctors()) {
        pending.add(course);
      }
      teacher.addProctor(course);
    }

    LOG.info("set proctors, add rest proctors"); 
    int index = 0;

    while (!pending.isEmpty()) {
      for (ICourse course : pending) {
        int last = index;
        while (!course.addProctor(teachers.get(index))) {
          index = index == (teachers.size() - 1) ? 0 : index + 1;
          if (index == last) {
            break;
          }
        }

        if (index != last) {
          teachers.get(index).addProctor(course);
        }
        
        if (course.hasEnoughProctors() && pending.contains(course)) {
          pending.remove(course);
        }

        index = index == (teachers.size() - 1) ? 0 : index + 1;
      }
    }
    
    // arrange for patrols
    LOG.info("add patrols");
    for (ICourse course : courses) {
      while (course.setPatrol(officials.get(patrolIndex))) {
        patrolIndex++;
      }
    }
    
    addBackups();
    
    for (ICourse course : courses) {
      while (course.setBackup(backups.get(backupIndex))) {
        backupIndex++;
      }
    }
  }


  public void addCourse(ICourse course) {
    courses.add(course);
  }

  public void addTeacher(ITeacher teacher) {
    teachers.add(teacher);
    
    if (teacher instanceof Official) {
      officials.add((Official) teacher);
    }
  }
  
  public void addFree(Time time, Classroom classroom) {
    DayHour dayHour = time.getDayHour();
    if (null == free.get(dayHour)) {
      Map<Time, Set<Classroom>> resourceMap = new HashMap<Time, Set<Classroom>>();
      free.put(dayHour, resourceMap);
    } else if (null == free.get(dayHour).get(time)) {
      free.get(dayHour).put(time, new HashSet<Classroom>());
    }
    free.get(dayHour).get(time).add(classroom); 
  }

  public void addFreeClassrooms(Time time, Set<Classroom> classroomSet) {
    for (Classroom classroom : classroomSet) {
      addFree(time, classroom);
    }
  }

  public void addFreeResource(Map<Time, Set<Classroom>> resourceMap) {
    for (Time time : resourceMap.keySet()) {
      free.put(time.getDayHour(), resourceMap);
    }
  }

  // teachers whose proctor counts are below average
  private void addBackups() {
    int sum = 0;
    for (ITeacher teacher : teachers) {
      sum += teacher.getCount();
    }
    int avg = sum / teachers.size();
    
    for (ITeacher teacher : teachers) {
      if (teacher.getCount() < avg) {
        backups.add(teacher);
      }
    }
  }
  
  
  public Map<Time, Set<Classroom>> getFreeByDayHour(DayHour dayHour) {
    return free.get(dayHour);
  }

  public Map<Time, Set<Classroom>> getFreeByTime(Time time) {
    return getFreeByDayHour(time.getDayHour());
  }

  public Pair<Time, Classroom> getFreeResource(CollegeYear collegeYear) {
    DayHour dayHour = defaultDayHourForCollegeYear(collegeYear);

    Map<Time, Set<Classroom>> freeResource = getFreeByDayHour(dayHour);
    for (Time time : freeResource.keySet()) {

      // no continuous exams for one college year at one morning or afternoon
      if (hasExamSameHalfDay(collegeYear, time)) {
        continue;
      }

      Set<Classroom> freeClassrooms = freeResource.get(time);
      if (freeClassrooms != null && !freeClassrooms.isEmpty()) {
        Iterator<Classroom> iterator = freeClassrooms.iterator();
        if (iterator.hasNext()) {
          Classroom classroom = iterator.next();
          updateCYSchedule(collegeYear, time);
          updateFree(time, classroom);
          return new Pair<Time, Classroom>(time, classroom);
        }
      }
    }

    return null;
  }

  private boolean hasExamSameHalfDay(CollegeYear collegeYear, Time time) {
    for (Time other : cySchedule.get(collegeYear)) {
      if (time.isSameHalfDay(other)) {
        return false;
      }
    }
    return true;
  }

  private void updateCYSchedule(CollegeYear collegeYear, Time time) {
    if (cySchedule.containsKey(collegeYear)) {
      cySchedule.get(collegeYear).add(time);
    }
  }

  private void updateFree(Time time, Classroom classroom) {
    DayHour dayHour = time.getDayHour();

    Map<Time, Set<Classroom>> resourceMap = free.get(dayHour);

    if (resourceMap != null && !resourceMap.isEmpty()) {
      Set<Classroom> classroomSet = resourceMap.get(time);
      if (classroomSet != null && !classroomSet.isEmpty()) {
        classroomSet.remove(classroom);
      }
      if (classroomSet.isEmpty()) {
        resourceMap.remove(time);
      }
    }
  }

  public Map<CollegeYear, DayHour> getDefaultSchedule() {
    return defaultSchedule;
  }

  public DayHour defaultDayHourForCollegeYear(CollegeYear collegeYear) {
    return defaultSchedule.get(collegeYear);
  }


}
